在上一篇中我們已經將 Django 的專案建立起來了,也新增了一個空的部落格應用程式,接下來是使用 Django ORM 來建立資料庫 Schema,首先先來看一個簡單的部落格應用程式資料庫可能會有哪些資料表,在這邊我使用實體關係圖(Entity Relationship Diagram)來做展示:
在圖上最右邊的auth_user
是 Django 內建的 auth 應用程式的使用者資料表,auth 應用程式本身是個完整的認證與授權模組,它內建還有其他的資料表,這邊為了聚焦在部落格應用本身,所以只顯示使用者資料表,在部落格應用程式上,一定會有文章資料表,如圖上blog_post
,而每篇文章都會有作者,作者(author_id
)關聯到使用者資料表,然後每篇文章可能會有多個標籤,會有標籤資料表,如圖上blog_tag
,文章跟標籤在資料庫的表示是多對多關係,所以會有一張中間的資料表blog_post_tags
來儲存多對多關係的資料,在這邊還會有文章的分類,如圖上blog_category
,文章可能會有多個分類,所以也是多對多關係,如圖上blog_post_categories
,然後會看到分類會有自我關聯,如圖上parent_id
欄位,用來標示分類會有階層關係,最後是文章的留言,如圖上blog_comment
資料表,留言一樣也會有多個階層的留言。
下面我們開始使用 Django 建立上面的資料表,打開檔案server/app/blog/models.py
新增以下內容:
from django.db import models
from django.conf import settings
from server.utils.django.models import BaseModel
USER_MODEL = settings.AUTH_USER_MODEL
class Post(BaseModel):
slug = models.SlugField("網址代稱", max_length=255, unique=True)
author = models.ForeignKey(
USER_MODEL,
verbose_name="作者",
on_delete=models.CASCADE,
)
title = models.CharField("標題", max_length=255)
content = models.TextField("內文")
published_at = models.DateTimeField("發布時間", null=True, blank=True)
published = models.BooleanField("是否發布", default=False)
tags = models.ManyToManyField(
"Tag",
verbose_name="標籤",
blank=True,
related_name="posts",
)
categories = models.ManyToManyField(
"Category",
verbose_name="分類",
blank=True,
related_name="posts",
)
def __str__(self) -> str:
return self.title
class Meta:
verbose_name = "文章"
verbose_name_plural = "文章"
ordering = ["-created_at"]
class Comment(BaseModel):
post = models.ForeignKey(
Post,
verbose_name="文章",
on_delete=models.CASCADE,
)
parent = models.ForeignKey(
"self",
verbose_name="上層留言",
on_delete=models.CASCADE,
null=True,
blank=True,
)
author = models.ForeignKey(
USER_MODEL,
verbose_name="作者",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
content = models.TextField("內文")
def __str__(self) -> str:
return self.content
class Meta:
verbose_name = "留言"
verbose_name_plural = "留言"
ordering = ["-created_at"]
class Tag(BaseModel):
name = models.CharField("標籤名稱", max_length=255, unique=True)
def __str__(self) -> str:
return self.name
class Meta:
verbose_name = "標籤"
verbose_name_plural = "標籤"
ordering = ["-created_at"]
class Category(BaseModel):
slug = models.SlugField("網址代稱", max_length=255, unique=True)
parent = models.ForeignKey(
"self",
verbose_name="上層分類",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
name = models.CharField("分類名稱", max_length=255)
def __str__(self) -> str:
if self.parent:
return f"{self.parent}/{self.name}"
return self.name
class Meta:
verbose_name = "分類"
verbose_name_plural = "分類"
ordering = ["-created_at"]
前面可以看到定義USER_MODEL
,它是使用 Django settings 中AUTH_USER_MODEL
設定使用者資料的模型,如果是自定義的使用者模型這種方式是比較好的 [1]。
在 Django 中使用ManyToManyField
預設會幫我們建立多對多關係的中間表。
前面提到的自我關聯,在 Django 是使用"self"
關鍵字來做到,另外在設計 Django ORM 模型時盡量以物件導向的思維來命名欄位名稱,像是parent
,Django 在將模型轉換成資料表時會自動將資料欄位命名成parent_id
。
新增完成我們需要的 Django 模型後,需要先產生資料庫遷移檔,然後才能透過資料庫遷移檔進行資料庫的異動。
我們先確定我們在django-graphql-tutorial
目錄下,先執行poetry shell
進到這個專案的 Python 虛擬環境,接著執行下面 Django 指令程式:
$ python manage.py makemigrations
會出現No changes detected
訊息,會覺得很奇怪,明明有新增這麼多個 Django 模型,怎麼會沒反應,這是因為忘記將我們部落格應用註冊進 Django 了。
在這邊使用的 Django 專案目錄結構與大部分的教學有所不同,需要對應用程式本身做一些額外設定,需要修改server/app/blog/apps.py
:
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "blog"
- name = "blog"
+ name = "server.app.blog"
+ verbose_name = "部落格"
name
要改成應用程式的路徑,並且加上verbose_name
之後在 Django admin 可以看到這邊設定的應用程式名稱。
設定完應用程式後,就到server/settings.py
中,找到INSTALLED_APPS
:
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
+ "server.app.blog",
]
註冊我們應用程式模組路徑到 Django 設定中。
接著我們在下一次建立資料庫遷移檔的指令:
$ python manage.py makemigrations
Migrations for 'blog':
server/app/blog/migrations/0001_initial.py
- Create model Category
- Create model Tag
- Create model Post
- Create model Comment
指令完成訊息會顯示建立的遷移檔,以及該遷移檔執行步驟的摘要,再接下來,我們要讓 Django 執行遷移檔內資料庫異動步驟,執行以下指令:
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying blog.0001_initial... OK
Applying sessions.0001_initial... OK
新的 Django 專案,第一次執行資料庫遷移,會執行許多內建應用程式的資料庫異動。
前面的 Django 實作 GraphQL 的練習會先做資料查詢,所以我們先使用 Django admin 作為簡易的資料管理後台,這邊是在server/app/blog/admin.py
中新增內容:
from django.contrib import admin
from server.app.blog.models import Category, Comment, Post, Tag
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "author", "published_at", "published"]
list_filter = ["published_at", "published"]
search_fields = ["title"]
autocomplete_fields = ["author", "tags", "categories"]
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ["post", "author", "parent", "content"]
list_filter = ["post"]
autocomplete_fields = ["post", "author"]
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ["name"]
search_fields = ["name"]
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ["name"]
search_fields = ["name"]
這邊將模型都註冊到 Django admin,並且在各自的 admin 類別稍微做一些設定:
list_display
:列表頁面要顯示哪些欄位。list_filter
:列表頁面右邊篩選可以使用哪些欄位作為篩選項目。search_fields
:列表頁面上面搜尋列,可以搜尋哪些欄位的資料。autocomplete_fields
:新增或編輯頁面中,表單中關聯的欄位有自動完成的功能。做到這邊,我們就可以先啟動 Django 開發伺服器了,執行下面指令:
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
Django version 4.2.5, using settings 'server.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
執行後,可以看到類似以上執行結果,我們就可以根據訊息,打開瀏覽器,瀏覽 Django admin 頁面http://127.0.0.1:8000/admin
。
會看到 Django admin 登入頁面,這時候還沒有帳號密碼,我們開啟新的終端機(terminal) session,進入 Python 虛擬環境,在專案下執行以下指令:
$ python manage.py createsuperuser
我們根據指令操作提示完成操作後,就可以再次登入 Django admin 頁面。
只有我們新增的應用程式是顯示中文,我可以再到server/settings.py
做些設定:
-LANGUAGE_CODE = "en-us"
+LANGUAGE_CODE = "zh-hant"
-TIME_ZONE = "UTC"
+TIME_ZONE = "Asia/Taipei"
上面設定語言代碼,以及時區。
存檔後,再回到 Django admin 頁面重新整理,就會到全中文的畫面:
最後我們就可以先在 Django 上新增一些資料:
最後我們要執行程式碼相關格式檢查前,在pyproject.toml
中加入忽略migrations目錄下內容的設定:
# ... 忽略
[tool.ruff]
# ... 忽略
+exclude = [
+ "**/migrations/*",
+]
# ... 忽略
+[tool.black]
+extend-exclude = '''
+/(
+ | migrations
+)/
+'''
接下來分別執行:
$ ruff check --fix .
$ pyright .
$ black .
這次修改內容可以參考 Git commit https://github.com/JiaWeiXie/django-graphql-tutorial/commit/b2502305be8f9a26f6aa600b6955e569b0c3c6b3。